package server.net.io;

import server.net.handler.ServerPacketHandler;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import server.net.handler.PongHandler;
import server.net.ClientConnection;
import server.net.handler.DisconnectRequestHandler;
import server.net.handler.GameListRequestHandler;
import server.net.handler.GameRequestHandler;
import server.net.handler.LoginHandler;
import server.net.handler.ObserveRequestHandler;
import server.net.handler.PlayerMoveHandler;
import server.net.handler.PlayerListRequestHandler;
import shared.net.protocol.ServerHeader.Receive;

/**
 * This class handles any incoming and outgoing messages from the clients using
 * a pool of worker threads.
 *
 * @author Vincent Tsuei
 * @version 2012-11-16
 */
public class ServerIOHandler {
    
    private static final int MAX_THREADS_PER_CORE = 2;
    private static final EnumMap<Receive, ServerPacketHandler> HandlerMap = new EnumMap<Receive, ServerPacketHandler>(Receive.class);
    private ThreadPoolExecutor workerThreadPool;

    public void init() {
        //Create a thread pool. This will allow work to be distributed among
        //various worker threads. It will also allow us to fail gracefully.
        int coresAvail = Runtime.getRuntime().availableProcessors();
        workerThreadPool = new ThreadPoolExecutor(Math.max(1, coresAvail), coresAvail * MAX_THREADS_PER_CORE, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
                new ThreadFactory() {
                    private final ThreadGroup tGroup = new ThreadGroup("WorkerPoolThreadGroup");
                    private int threadCounter = 1;

                    //It should be okay to use synchronized here, we shouldn't be creating many threads.
                    @Override
                    public synchronized Thread newThread(Runnable r) {
                        return new Thread(tGroup, r, "WorkerPoolThread-" + (threadCounter++));
                    }
                });
        
        /**
         * Register the handlers here.
         */
        HandlerMap.put(Receive.PONG, new PongHandler()); //Complete
        HandlerMap.put(Receive.LOGIN_REQUEST, new LoginHandler()); //Complete
        HandlerMap.put(Receive.DISCONNECT_REQUEST, new DisconnectRequestHandler()); //@Deprecated.
        
        HandlerMap.put(Receive.GAME_LIST_REQUEST, new GameListRequestHandler()); //Complete
        HandlerMap.put(Receive.PLAYER_LIST_REQUEST, new PlayerListRequestHandler()); //Complete
        
        HandlerMap.put(Receive.NEW_GAME_REQUEST, new GameRequestHandler()); //Vince - Working on this.
        
        HandlerMap.put(Receive.OBSERVE_GAME_REQUEST, new ObserveRequestHandler()); 
        HandlerMap.put(Receive.PLAYER_MOVE_REQUEST, new PlayerMoveHandler());
    }

    /**
     * Handle any incoming data from the client.
     */
    private static int READ_BUFFER_SIZE = 2048; //In Bytes
    public void handleReadEvent(ClientConnection con) {
        ByteBuffer buffer;

        //Check if there was a previous buffer.
        //If there is, move the remaining buffer into a new one, and clear the remaining buffer.
        //If there is not, create a new buffer.
        if (con.hasRemainingBuffer()) {
            ByteBuffer rem = con.getRemainingBuffer();
            buffer = ByteBuffer.allocate(rem.remaining() + READ_BUFFER_SIZE + 1); //+1 for the sake of safety
            buffer.put(rem);
            con.clearRemainingBuffer();
        } else {
            buffer = ByteBuffer.allocate(READ_BUFFER_SIZE);
        }

        //Read in the new data from the SocketChannel
        try {
            int i = con.getSocketChannel().read(buffer);
            if (i == -1) {
                //Shutdown the connection if EOF is read.
                con.shutdown();
                Logger.getLogger(ServerIOHandler.class.getName()).log(Level.FINE, "EOF Received from client: {0}. Closing connection.", new Object[]{con.getAddressAndPortString()});
            }
            buffer.flip();
            Logger.getLogger(ServerIOHandler.class.getName()).log(Level.FINEST, "handleReadEvent | buffer: {0} - {1}", new Object[] {buffer.toString(), Arrays.toString(buffer.array())});
            
            //Turn the byte stream into a packet stream
            //  a. Read in as many packets as we have received.
            //  b. If we only have a partial packet, let it sit in the buffer.
            
            //Special case:
            //  If there is less than 4 bytes, we cannot even read the size. Wait for more information.
            if (buffer.remaining() < 4) {
                con.setRemainingBuffer(buffer);
            } else {
                int packetSize = buffer.getInt();
                boolean unreadSize = true;
                //System.out.println(packetSize + " " + buffer.remaining()); //Debug code
                
                while (buffer.remaining() >= packetSize) {
                    //System.out.println("Received Packet. Size: " + packetSize);

                    //Read in the data and wrap the packet into a new ByteBuffer for processing
                    byte[] packetData = new byte[packetSize];
                    buffer.get(packetData, 0, packetSize);
                    ByteBuffer packet = ByteBuffer.wrap(packetData);

                    //Comment logger line for production release
                    if (packetData[0] != 0) //Ignore Ping
                    if (con.getClient().isLoggedIn()) {
                        Logger.getLogger(ServerIOHandler.class.getName()).log(Level.INFO, "Received Message from {1}: {0} | {2}", 
                                new Object[] {new String(packetData), con.getClient().getPlayer().getName(), Arrays.toString(packetData)});
                    } else {
                        Logger.getLogger(ServerIOHandler.class.getName()).log(Level.INFO, "Received Message: {0} | {1}", new Object[] {new String(packetData), Arrays.toString(packetData)});
                    }
                    //Send the packet off to be processed by worker threads.
                    workerThreadPool.submit(new WorkUnit(this, con.getClient(), packet));

                    //Last Step: Check if there is more information
                    if (buffer.remaining() < 4) {
                        if (buffer.remaining() == 0) {
                            unreadSize = false;
                        }
                        break;
                    } else {
                        packetSize = buffer.getInt();
                    }
                }
                if (unreadSize) {
                    buffer.position(buffer.position() - 4); //Pretend we have not read the size yet. - Hackyish?
                    con.setRemainingBuffer(buffer);
                }
            }
        } catch (IOException ex) {
            if (ex.getMessage().equals("An existing connection was forcibly closed by the remote host")) { //Connection throws this specific message if a client forcibly quits the program.
                Logger.getLogger(ServerIOHandler.class.getName()).log(Level.INFO, "Connection terminated by client: {0}", new Object[]{con.getAddressAndPortString()});
            } else {
                Logger.getLogger(ServerIOHandler.class.getName()).log(Level.SEVERE, null, ex);
            }
            con.getClient().disconnect();
            //con.shutdown();
        }
    }
    
    /**
     * Get the handler for a certain Receive header.
     */
    public ServerPacketHandler getHandler(Receive r) {
        return HandlerMap.get(r);
    }

    /**
     * Handle a write request.
     */
    public void handleWriteEvent(ClientConnection con) {
        con.doWrite(); //Pass it off to the NimConnection class.
    }
}
